package org.jing.ext.ftp;

import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.jing.core.lang.JingException;
import org.jing.core.logger.JingLogger;
import org.jing.core.util.GenericUtil;
import org.jing.core.util.StringUtil;
import org.jing.ext.example.Example4FtpOperator;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;

/**
 * Description: FTP操作对象. <br>
 *     建议使用申请资源方式创建对象. <br>
 *
 * @author bksmate <br>
 * @see Example4FtpOperator#Example4FtpOperator()  参考用例 <br>
 * @since 2021-02-02 <br>
 */
@SuppressWarnings({ "WeakerAccess", "unused", "UnusedReturnValue", "Duplicates" })
public class FtpOperator implements Closeable {
    private static final JingLogger LOGGER = JingLogger.getLogger(FtpOperator.class);

    private final FTPClient ftpClient;

    private final int hashCode;

    private boolean closed = false;

    private FtpOperator() {
        ftpClient = new FTPClient();
        hashCode = hashCode();
    }

    @Override protected void finalize() throws Throwable {
        try {
            if (!closed) {
                bye();
            }
        }
        catch (Throwable ignored) {}
        super.finalize();
    }

    /**
     * Description: 指定参数获取封装操作对象. <br>
     * @param ip IP地址 <br>
     * @param port 端口 <br>
     * @param name 登录名 <br>
     * @param password 登录密码 <br>
     * @param timeout 超时时间 <br>
     * @param encoding 系统编码, 可为空(不设置) <br>
     * @return 封装操作对象  <br>
     */
    public static FtpOperator connect(String ip, int port, String name, String password, int timeout, String encoding) {
        FtpOperator ftpOperator = new FtpOperator();
        LOGGER.debug("[{}] Connect to ftp", ftpOperator.hashCode);
        try {
            LOGGER.debug("[{}] Timeout: {}", ftpOperator.hashCode, timeout);
            ftpOperator.ftpClient.setConnectTimeout(timeout);
            LOGGER.debug("[{}] Address: {}:{}", ftpOperator.hashCode, ip, port);
            ftpOperator.ftpClient.connect(ip, port);
            LOGGER.debug("[{}] User: {}", ftpOperator.hashCode, name);
            ftpOperator.ftpClient.login(name, password);
            int replayCode =ftpOperator.ftpClient.getReplyCode();
            LOGGER.debug("[{}] Connection result: {}", ftpOperator.hashCode, replayCode);
            if (FTPReply.isPositiveCompletion(replayCode)) {
                ftpOperator.setEncoding(encoding);
                LOGGER.debug("[{}] Success to connect ftp", ftpOperator.hashCode);
                return ftpOperator;
            }
            else {
                throw new JingException(StringUtil.mixParameters("[{}] Unexpected replay code: [{}]", ftpOperator.hashCode, replayCode));
            }
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 断开连接. <br>
     *     如果不主动调用, 析构函数里也会调用, 但还是建议自己手动关闭连接. <br>
     */
    public void bye() {
        try {
            if (closed) {
                LOGGER.debug("[{}] Ftp has closed", hashCode);
            }
            else {
                synchronized (ftpClient) {
                    ftpClient.logout();
                    LOGGER.debug("[{}] Ftp closed", hashCode);
                    closed = true;
                }
            }
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    private void validateActive() {
        synchronized (ftpClient) {
            if (closed) {
                throw new JingException("Ftp client has already closed");
            }
        }
    }

    /**
     * Description: 切换目录. <br>
     * @param path 指定目录 <br>
     * @return true:成功/false:失败 <br>
     */
    public boolean cd(String path) {
        validateActive();
        boolean result;
        LOGGER.debug("[{}] Change directory to path: [path: {}]", hashCode, path);
        if (StringUtil.isEmpty(path)) {
            LOGGER.warn("[{}] Cancel change because of empty path", hashCode);
            return true;
        }
        try {
            LOGGER.debug("[{}] Try to change directory by full path: [path: {}][result: {}]", hashCode, path, result = ftpClient.changeWorkingDirectory(path));
            if (result) {
                return true;
            }
            LOGGER.debug("[{}] Failed to change directory by full path, try to change by each sub directory", hashCode);
            int length = path.length();
            char c;
            StringBuilder stbr = new StringBuilder();
            stbr.append(c = path.charAt(0));
            String subPath;
            for (int i$ = 1; i$ <= length; i$++) {
                if (i$ != length) {
                    c = path.charAt(i$);
                    stbr.append(c);
                }
                if (c == '/' || i$ == length) {
                    if (stbr.length() == 0) {
                        break;
                    }
                    subPath = stbr.toString();
                    LOGGER.debug("[{}] Change to sub directory: [subPath: {}][result: {}]", hashCode, subPath, result = ftpClient.changeWorkingDirectory(subPath));
                    if (!result) {
                        LOGGER.debug("[{}] Try to make directory: [subPath: {}][result: {}]", hashCode, subPath, result = ftpClient.makeDirectory(subPath));
                        if (!result) {
                            LOGGER.warn("[{}] Failed to make sub directory", hashCode);
                            return false;
                        }
                        LOGGER.debug("[{}] Change to sub directory: [subPath: {}][result: {}]", hashCode, subPath, result = ftpClient.changeWorkingDirectory(subPath));
                        if (!result) {
                            LOGGER.warn("[{}] Failed to change directory to sub directory", hashCode);
                            return false;
                        }
                    }
                    stbr.setLength(0);
                }
            }
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
        LOGGER.debug("[{}] Success to change directory: [path: {}]", hashCode, path);
        return true;
    }

    /**
     * Description: 指定FTP文件传输的传输类型. <br>
     * @param ftpFileType 文件传输类型 <br>
     * @return <br>
     */
    public boolean changeFtpFileType(FtpFileType ftpFileType) {
        validateActive();
        boolean result;
        try {
            LOGGER.debug("[{}] Set Ftp file type to {}: {}", hashCode, ftpFileType.getFileTypeName(), result = ftpClient.setFileType(ftpFileType.getFileType()));
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
        return result;
    }

    /**
     * Description: 进入被动端口模式. <br>
     */
    public void enterLocalPassiveMode() {
        LOGGER.debug("[{}] Enter local passive model", hashCode);
        ftpClient.enterLocalPassiveMode();
    }

    /**
     * Description: 指定系统编码. <br>
     * @param encoding <br>
     */
    public void setEncoding(String encoding) {
        validateActive();
        if (StringUtil.isNotEmpty(encoding)) {
            LOGGER.debug("[{}] Set encoding: {}", hashCode, encoding);
            ftpClient.setControlEncoding(encoding);
        }
    }

    /**
     * Description: 列举当前目录的所有文件. <br>
     * @return <br>
     */
    public FTPFile[] ls() {
        validateActive();
        try {
            LOGGER.debug("[{}] Ftp list all files...", hashCode);
            FTPFile[] files = ftpClient.listFiles();
            int size = GenericUtil.count(files);
            FTPFile file;
            for (int i$ = 0; i$ < size; i$++) {
                file = files[i$];
                LOGGER.debug("[name: {}][type: {}][size: {} bytes]", file.getName(), file.isFile() ? "file" : "directory", file.isFile() ? file.getSize() : "?");
            }
            return files;
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 列举当前目录的指定文件名的文件.  <br>
     * @param name 指定文件名 <br>
     * @return <br>
     */
    public FTPFile ls(String name) {
        validateActive();
        try {
            FTPFile[] files = ls();
            int size = GenericUtil.count(files);
            FTPFile file, retFile = null;
            for (int i$ = 0; i$ < size && null == retFile; i$++) {
                file = files[i$];
                if (file.getName().equals(name)) {
                    retFile = file;
                }
            }
            LOGGER.debug("[{}] List file [{}]: [name: {}][type: {}][size: {} bytes]", hashCode, name,
                null == retFile ? "" : retFile.getName(),
                null == retFile ? "" : retFile.isFile() ? "file" : "directory",
                null == retFile ? "" : retFile.isFile() ? retFile.getSize() : "?");
            return retFile;
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
    }

    /**
     * Description: 上传本地文件到FTP当前目录的指定文件名. <br>
     * @param file 本地文件 <br>
     * @param destFileName FTP当前目录指定文件名 <br>
     * @return <br>
     */
    public boolean put(File file, String destFileName) {
        validateActive();
        boolean result;
        LOGGER.debug("[{}] Put file {} to {}", hashCode, file.getAbsolutePath(), destFileName);
        try (InputStream inputStream = Files.newInputStream(file.toPath())) {
            LOGGER.debug("[{}] Put file result: {}", hashCode, result = ftpClient.storeFile(destFileName, inputStream));
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
        return result;
    }

    /**
     * Description: 上传本地文件到FTP当前目录的同名文件.  <br>
     * @param file 本地文件 <br>
     * @return <br>
     */
    public boolean put(File file) {
        return put(file, file.getName());
    }

    /**
     * Description: 上传本地文件路径的文件到FTP当前目录的指定文件名.  <br>
     * @param filePath 本地文件路径 <br>
     * @param destFileName FTP当前目录指定文件名 <br>
     * @return <br>
     */
    public boolean put(String filePath, String destFileName) {
        return put(new File(filePath), destFileName);
    }

    /**
     * Description: 上传本地文件路径的文件到FTP当前目录的同名文件.  <br>
     * @param filePath 本地文件路径 <br>
     * @return <br>
     */
    public boolean put(String filePath) {
        return put(new File(filePath));
    }

    /**
     * Description: 获取FTP指定文件到本地文件里. <br>
     * @param fileName FTP指定文件名 <br>
     * @param destFile 本地文件 <br>
     * @return <br>
     */
    public boolean get(String fileName, File destFile) {
        validateActive();
        boolean result;
        LOGGER.debug("[{}] Get file {} to {}", hashCode, fileName, destFile.getAbsolutePath());
        try (OutputStream outputStream = Files.newOutputStream(destFile.toPath())) {
            LOGGER.debug("[{}] Get file result: {}", hashCode, result = ftpClient.retrieveFile(fileName, outputStream));
        }
        catch (JingException e) {
            throw e;
        }
        catch (Throwable t) {
            throw new JingException(t);
        }
        return result;
    }

    /**
     * Description: 获取FTP指定文件到本地指定文件名的文件里. <br>
     * @param fileName FTP指定文件名 <br>
     * @param destFileName 本地指定文件名 <br>
     * @return <br>
     */
    public boolean get(String fileName, String destFileName) {
        return get(fileName, new File(destFileName));
    }

    /**
     * Description: 获取FTP指定文件到本地同名文件里. <br>
     * @param fileName FTP指定文件名 <br>
     * @return <br>
     */
    public boolean get(String fileName) {
        return get(fileName, new File(fileName));
    }

    @Override public void close() throws IOException {
        try {
            bye();
        }
        catch (Throwable e) {
            throw new IOException(e);
        }
    }

    public FTPClient getCoreClient() {
        return ftpClient;
    }
}
